# -*- coding: utf-8 -*-
"""
Native Python RPC Layer
"""
from debugtools import *
from codepickle import *
from connection import *
from multiprocessing import Process
import multiprocessing, multiprocessing.connection, threading, logging
import os, sys, zlib, cPickle, time, traceback, gc, socket, base64, math, binascii
from numpy.random import randint

__all__ = ['DEFAULT_PORT', 'BaseRpcServer', 'BaseRpcClient', 'BaseRpcClients',
           'open_base_server', 'close_base_servers']

DEFAULT_PORT = 2718

def open_base_server(port = None):
    BaseRpcServer(port=port).listen()

def close_base_servers(addresses):
    if type(addresses) is str:
        addresses = [(addresses, DEFAULT_PORT)]
    if type(addresses) is tuple:
        addresses = [addresses]
    BaseRpcClients(addresses).close_server()




class DistantException(Exception):
    """
    Distant Exception class. Allows to re-raise exception on the client, giving 
    the filename and the line number of the original server-side exception. 
    """
    def __init__(self, exception = None):
        if type(exception) == str:
            exception = Exception(exception)
        self.exception = exception
        try:
            self.filename = __file__
            if os.path.splitext(__file__)[1] == '.pyc':
                self.filename = __file__[:-1]
        except:
            self.filename = 'unknown'
        try:
            self.line = sys._getframe(1).f_lineno
        except:
            self.line = 0
        try:
            self.function = sys._getframe(1).f_code.co_name
        except:
            self.function = 'unknown'
    
    def setinfo(self):
        (self.filename, self.line, self.function, self.text) = traceback.extract_tb(sys.exc_info()[2])[-1]

    def __str__(self):
        s = "A distant exception happened in:"
        s += "\n  File \"%s\", line %d, in %s" % (self.filename, self.line, str(self.function))
        s += "\n    %s" % str(self.exception)
        return s



class BaseRpcServer(object):
    def __init__(self, port = None, bindip = ''):
        """
        to be implemented by a deriving class:
        initialize()
        process(client, procedure)
        shutdown()
        """
        if port is None:
            port = DEFAULT_PORT
        self.port = port
        self.address = (bindip, self.port)
        self.bool_shutdown = False
        
    def serve(self, conn, client):
        # called in a new thread
        keep_connection = None # allows to receive several procedures during the same session
        # None : close connection at the next iteration
        # False : close connection now
        # True : keep connection for now
        
        while keep_connection is not False:
            procedure = conn.recv()
            log.debug("server: procedure '%s' received" % procedure)
            
            if procedure == 'keep_connection':
                keep_connection = True
                continue # immediately waits for a procedure
            elif procedure == 'close_connection':
                keep_connection = False
                break # close connection
            elif procedure == 'shutdown':
                log.debug("server: shutdown signal received")
                keep_connection = False
                self.bool_shutdown = True
                break # closes the connection immediately
            
            # Dispatches the procedure to the handler unless the procedure
            # asks the server to close the handler or itself
            log.debug("server: processing procedure")
            result = self.process(client, procedure)
                
            # returns the result to the client
            log.debug("server: returning the result to the client")
            conn.send(result)
            
            if keep_connection is None:
                keep_connection = False
        
        if conn is not None:
            conn.close()
            conn = None
        log.debug("server: connection closed")
    
    def listen(self):
        """
        Listens to incoming connections and create one handler for each new connection.
        """
        conn = None
        threads = []
        index = 0
        
        # Initializing
        log.info("server: initializing")
        self.initialize()
        
        while not self.bool_shutdown:
            try:
                log.debug("server: waiting for incoming connection on port %d..." % self.address[1])
                conn, client = accept(self.address) # accepts an incoming connection
                log.info("server: connection established with client '%s' on port %d" % (client, self.address[1]))
            except:
                log.warn("server: connection NOT established, closing now")
                break
            
            thread = threading.Thread(target=self.serve, args=(conn, client))
            thread.start()
            threads.append(thread)
            index += 1
            time.sleep(.1)

        # Closes the connection at the end of the server lifetime
        if conn is not None:
            conn.close()
            conn = None
        
        for i in xrange(len(threads)):
            log.debug("joining thread %d/%d" % (i+1, len(threads)))
            threads[i].join()
            
        # closing
        log.info("server: closing")
        self.shutdown()

    def initialize(self):
        log.warn("server: 'initialize' method not implemented")

    def process(self, client, procedure):
        log.warn("server: 'process' method not implemented")
        return procedure
    
    def shutdown(self):
        log.warn("server: 'shutdown' method not implemented")




class BaseRpcClient(object):
    """
    RPC Client constructor.
    """
    def __init__(self, server):
        self.conn = None
        if type(server) is tuple:
            server, port = server
        elif type(server) is str:
            port = None
        self.server = server
        if port is None:
            port = DEFAULT_PORT
        self.port = port
        self.keep_connection = False
    
    def open_connection(self):
        log.debug("client: connecting to '%s' on port %d" % (self.server, self.port))
        self.conn = connect((self.server, self.port))
    
    def close_connection(self):
        log.debug("client: closing connection")
        self.conn.close()
        self.conn = None 
    
    def is_connected(self):
        return self.conn is not None
    
    def execute(self, procedure):
        """
        Calls a procedure on the server from the client.
        """
        if not self.is_connected():
            self.keep_connection = False
            self.open_connection()
        
        log.debug("client: sending procedure")
        try:
            self.conn.send(procedure)
        except:
            log.warn("client: connection lost while sending the procedure, connecting again...")
            self.open_connection()
            self.conn.send(procedure)

        log.debug("client: receiving result")
        try:
            result = self.conn.recv()
        except:
            log.warn("client: connection lost while retrieving the result, connecting again...")
            self.open_connection()
            result = self.conn.recv()
        
        if not self.keep_connection:
            self.close_connection()
        
        # re-raise the Exception on the client if one was raised on the server
        if isinstance(result, Exception):
            raise result
        # Handling list of exceptions
        if type(result) is list:
            exceptions = []
            for r in result:
                if isinstance(r, Exception):
                    exceptions.append(str(r))
            if len(exceptions)>0:
                raise Exception("\n".join(exceptions))
        
        return result

    def connect(self):
        self.keep_connection = True
        self.open_connection()
        self.conn.send('keep_connection')
        
    def disconnect(self):
        self.keep_connection = False
        self.conn.send('close_connection')
        self.close_connection()

    def close_server(self):
        """
        Closes the server from the client.
        """
        if not self.is_connected():
            self.open_connection()
        log.debug("client: sending the shutdown signal")
        self.conn.send('shutdown')
        self.close_connection()
        
        

class BaseRpcClients(object):
    def __init__(self, servers):
        self.servers = servers
        
        self.clients = [BaseRpcClient(server) for server in servers]
        self.results = {}
    
    def open_threads(self, targets, argss = None):
        if argss is None:
            argss = [()]*len(self.servers)
        threads = []
        for i in xrange(len(self.servers)):
            thread = threading.Thread(target=targets[i], 
                                      args=argss[i])
            thread.start()
            threads.append(thread)
        [thread.join() for thread in threads]
    
    def _execute(self, index, procedure):
        try:
            self.results[index] = self.clients[index].execute(procedure)
        except Exception as inst:
            self.exceptions.append(str(inst))
    
    def execute(self, procedures):
        """
        Makes simultaneously (multithreading) several calls to different RPC servers.
        """
        i = 0
        results = []
        threads = []
        self.exceptions = []
       
        self.open_threads([self._execute]*len(self.servers), [(i, procedures[i]) for i in xrange(len(self.servers))])

        if len(self.exceptions)>0:
            raise Exception("\n".join(self.exceptions))
        for i in xrange(len(self.servers)):
            if i in self.results.keys():
                result = self.results[i]
            else:
                result = None
            results.append(result)
        return results

    def connect(self):
        self.open_threads([client.connect for client in self.clients])
        
    def disconnect(self):
        self.open_threads([client.disconnect for client in self.clients])

    def close_server(self):
        self.open_threads([client.close_server for client in self.clients])
